﻿#region License
// (c) Intergen.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
#endregion

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using TextGlow.Control.Controls.Document;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using TextGlow.Control.Utilities;
using System.Linq;
using System.Text;
using System.Windows.Browser;
using System.IO;
using System.Reflection;
using System.Windows.Markup;
using System.Collections;

namespace TextGlow.Control.Controls.Document
{
  public partial class ParagraphControl : UserControlExtended, ISplittable
  {
    private bool _hasSplit;
    private double _availableWidth;

    private double _spacingBefore;
    private double _spacingAfter;
    private Unit _spacingLine;

    private bool _precededBySameBorders;
    private bool _followedBySameBorders;

    private double _indentLeft;
    private double _indentRight;
    private double _indentFirstLine;
    private double _indentHanging;

    private double _defaultTabStop;

    private Border _borderBottom;
    private Border _borderTop;
    private Border _borderLeft;
    private Border _borderRight;

    //private Line _borderBottomLine;
    //private Line _borderTopLine;
    //private Line _borderLeftLine;
    //private Line _borderRightLine;

    //private StackPanel _lineContainer;

    private ParagraphLine _previousLine;
    private ParagraphLine _currentLine;

    private Brush _background;

    private readonly Collection<InlineControl> _inlines = new Collection<InlineControl>();
    private readonly List<TabStop> _tabStops = new List<TabStop>();

    //public StackPanel LineContainer
    //{
    //  get { return _lineContainer; }
    //}

    public List<TabStop> TabStops
    {
      get { return _tabStops; }
    }

    public Collection<InlineControl> Inlines
    {
      get { return _inlines; }
    }

    public bool PrecededBySameBorders
    {
      get { return _precededBySameBorders; }
      set { _precededBySameBorders = value; }
    }

    public bool FollowedBySameBorders
    {
      get { return _followedBySameBorders; }
      set { _followedBySameBorders = value; }
    }

    public Unit SpacingLine
    {
      get { return _spacingLine; }
      set { _spacingLine = value; }
    }

    public double DefaultTabStop
    {
      get { return _defaultTabStop; }
      set { _defaultTabStop = value; }
    }

    public new Brush Background
    {
      get { return _background; }
      set { _background = value; }
    }

    public Border BorderTop
    {
      get { return _borderTop; }
      set { _borderTop = value; }
    }

    public Border BorderLeft
    {
      get { return _borderLeft; }
      set { _borderLeft = value; }
    }

    public Border BorderRight
    {
      get { return _borderRight; }
      set { _borderRight = value; }
    }

    public Border BorderBottom
    {
      get { return _borderBottom; }
      set { _borderBottom = value; }
    }

    public double SpacingBefore
    {
      get { return _spacingBefore; }
      set { _spacingBefore = value; }
    }

    public double SpacingAfter
    {
      get { return _spacingAfter; }
      set { _spacingAfter = value; }
    }

    public double IndentLeft
    {
      get { return _indentLeft; }
      set { _indentLeft = value; }
    }

    public double IndentFirstLine
    {
      get { return _indentFirstLine; }
      set { _indentFirstLine = value; }
    }

    public double IndentRight
    {
      get { return _indentRight; }
      set { _indentRight = value; }
    }

    public double IndentHanging
    {
      get { return _indentHanging; }
      set { _indentHanging = value; }
    }

    public bool HasSplit
    {
      get { return _hasSplit; }
      set { _hasSplit = value; }
    }

    public ParagraphControl()
    {
      InitializeComponent();

      //_backgroundCanvas = (Canvas)InnerElement.FindName("BackgroundCanvas");

      //_borderBottomLine = (Line)InnerElement.FindName("BorderBottomLine");
      //_borderTopLine = (Line)InnerElement.FindName("BorderTopLine");
      //_borderLeftLine = (Line)InnerElement.FindName("BorderLeftLine");
      //_borderRightLine = (Line)InnerElement.FindName("BorderRightLine");
      //_lineContainer = (StackPanel)InnerElement.FindName("LineContainer");

      _borderBottom = new Border();
      _borderTop = new Border();
      _borderLeft = new Border();
      _borderRight = new Border();

      Loaded += new RoutedEventHandler(ParagraphControl_Loaded);
    }

    void ParagraphControl_Loaded(object sender, RoutedEventArgs e)
    {
      //IEnumerable<KeyValuePair<object, object>> r = (IEnumerable<KeyValuePair<object, object>>)Resources;
      //int c = r.Where(kv => kv.Value is Storyboard).Count();

      //foreach (KeyValuePair<object, object> resource in Resources)
      //{
      //  Storyboard sb = resource.Value as Storyboard;
      //  if (sb != null)
      //    sb.Begin();
      //}
    }

    private double CalculatePreBorderSpace()
    {
      return (!_precededBySameBorders) ? _spacingBefore : 0;
    }

    private double CalculatePostBorderSpace()
    {
      return _borderTop.Spacing;
    }

    protected override Size CustomArrangeOverride(Size finalSize)
    {
      LineContainer.CustomArrange(LayoutUtils.CreateRect(LayoutUtils.Zero, LineContainer.CustomDesiredSize));
      //LayoutRoot.SetLocation(new Point(-100, 0));
      //LineContainer.SetLocation(new Point(-100, 0));

      return finalSize;
    }

    protected override Size CustomMeasureOverride(Size availableSize)
    {
      _availableWidth = availableSize.Width;

      Point borderTopLeft = new Point(-_borderLeft.Spacing, CalculatePreBorderSpace());
      Point textBlockTopLeft = new Point(_borderLeft.Spacing, _borderTop.Spacing + ((_precededBySameBorders) ? _spacingBefore : 0));
      Size textBlockSize = new Size(availableSize.Width - _indentRight, double.PositiveInfinity);

      //LineContainer.ItemWidth = textBlockSize.Width;
      LineContainer.Width = textBlockSize.Width;
      LineContainer.MaxWidth = textBlockSize.Width;
      LineContainer.MinWidth = textBlockSize.Width;

      if (LineContainer.Children.Count == 0)
        CreateLines();

      LineContainer.CustomMeasure(textBlockSize);
      LineContainer.SetLocation(textBlockTopLeft);

      Rect textBlockRect = LayoutUtils.CreateRect(textBlockTopLeft, new Size(LineContainer.CustomDesiredSize.Width,
        LineContainer.CustomDesiredSize.Height));

      Size borderSize = new Size(textBlockRect.Right + _borderRight.Spacing,
        textBlockRect.Bottom + _borderBottom.Spacing + ((!_followedBySameBorders) ? 0 : _spacingAfter));

      Rect borderRect = LayoutUtils.CreateRect(borderTopLeft, borderSize);


      if (_background != null)
      {
        BackgroundCanvas.Background = _background;
        BackgroundCanvas.SetBounds(borderRect);
      }

      if (_borderBottom.IsVisible)
      {
        BorderBottomLine.Stroke = new SolidColorBrush(_borderBottom.Color);
        BorderBottomLine.StrokeThickness = _borderBottom.Width;
        BorderBottomLine.SetBounds(new Rect(borderRect.GetBottomLeft(), borderRect.GetBottomRight()));
      }
      if (_borderTop.IsVisible)
      {
        BorderTopLine.Stroke = new SolidColorBrush(_borderTop.Color);
        BorderTopLine.StrokeThickness = _borderTop.Width;
        BorderTopLine.SetBounds(new Rect(borderRect.GetTopLeft(), borderRect.GetTopRight()));
      }
      if (_borderLeft.IsVisible)
      {
        BorderLeftLine.Stroke = new SolidColorBrush(_borderLeft.Color);
        BorderLeftLine.StrokeThickness = _borderLeft.Width;
        BorderLeftLine.SetBounds(new Rect(borderRect.GetTopLeft(), borderRect.GetBottomLeft()));
      }
      if (_borderRight.IsVisible)
      {
        BorderRightLine.Stroke = new SolidColorBrush(_borderRight.Color);
        BorderRightLine.StrokeThickness = _borderRight.Width;
        BorderRightLine.SetBounds(new Rect(borderRect.GetTopRight(), borderRect.GetBottomRight()));
      }

      double height = Math.Max(borderRect.Bottom + ((!_followedBySameBorders) ? _spacingAfter : 0), 0);

      return new Size(availableSize.Width, height);
    }

    /// <summary>
    /// Splits the inlines on line break characters.
    /// </summary>
    /// <param name="inlines">The inlines.</param>
    /// <returns></returns>
    private static IList<InlineControl> SplitInlines(IList<InlineControl> inlines)
    {
      List<InlineControl> splitInlines = new List<InlineControl>();

      foreach (InlineControl inline in inlines)
      {
        if (inline is HyperLinkControl)
        {
          HyperLinkControl hyperlink = WpfUtils.Clone<HyperLinkControl>((HyperLinkControl)inline);
          IList<InlineControl> hyperlinkSplitInlines = SplitInlines(hyperlink.Inlines);
          hyperlink.Inlines.Clear();
          hyperlink.Inlines.AddRange(hyperlinkSplitInlines);

          splitInlines.Add(inline);
        }
        else if (inline is RunControl)
        {
          RunControl run = (RunControl)inline;
          IList<string> textLines = run.Text.Split('\u000A', '\u000D', '\u001C', '\u001E', '\u0085', '\u2029', /*Used by blend for breaklines */ '\u2028');

          if (textLines.Count == 1)
          {
            splitInlines.Add(run);
          }
          else
          {
            bool first = true;

            foreach (string textLine in textLines)
            {
              if (first)
                first = false;
              else
                splitInlines.Add(new LineBreakControl());

              RunControl splitRun = WpfUtils.Clone<RunControl>(run);
              splitRun.Text = textLine;
              splitInlines.Add(splitRun);
            }
          }
        }
        else
        {
          splitInlines.Add(inline);
        }
      }

      return splitInlines;
    }

    private void AddLine()
    {
      _previousLine = _currentLine;
      _currentLine = new ParagraphLine();
      _currentLine.SpacingLine = _spacingLine;
      //_currentLine.Width = _availableWidth;
      _currentLine.MaxWidth = _availableWidth;
      _currentLine.HorizontalAlignment = HorizontalAlignment;

      bool firstLine = LineContainer.Children.IsEmpty();

      // add invisible spacer controls to simulate indenting
      if (_indentLeft > 0)
      {
        _currentLine.Children.Add(CreateSpacer(Math.Max((firstLine) ? _indentLeft - _indentHanging : _indentLeft, 0)));
      }
      if (firstLine && _indentFirstLine > 0)
      {
        _currentLine.Children.Add(CreateSpacer(_indentFirstLine));
      }

      LineContainer.Children.Add(_currentLine);
    }

    private UIElement CreateSpacer(double width)
    {
      Canvas spacer = new Canvas();
      spacer.Height = 1;
      spacer.Width = width;
      //spacer.Background = new SolidColorBrush(Color.FromArgb(255, 0, 0, 0));

      return spacer;
    }

    private void CreateLines()
    {
      // split inlines up on any line breaking characters
      List<InlineControl> splitInlines = new List<InlineControl>(SplitInlines(_inlines).Reverse());
      List<ParagraphLine> paragraphLines = new List<ParagraphLine>();

      // loop through the inlines, adding them to lines until none are left
      while (!splitInlines.IsEmpty())
      {
        InlineControl currentInline = splitInlines[splitInlines.Count - 1];
        splitInlines.RemoveAt(splitInlines.Count - 1);
        AddInline(currentInline, null);
      }
    }

    private void AddInline(InlineControl currentInline, HyperLinkControl currentHyperLink)
    {
      if (_currentLine == null)
        AddLine();

      if (currentInline is LineBreakControl)
      {
        // TODO
        _currentLine.Children.Add(new TextBlock() { Text = "", FontSize = 12 }); // ?

        _currentLine = null;
      }
      else if (currentInline is RunControl)
      {
        RunControl run = (RunControl)currentInline;
        TextBlock textblock = new TextBlock();
        textblock.Text = run.Text;
        textblock.TextDecorations = run.TextDecorations;
        textblock.Foreground = run.Foreground;
        textblock.FontStyle = run.FontStyle;
        textblock.FontSize = run.FontSize;
        textblock.FontFamily = run.FontFamily;
        textblock.FontWeight = run.FontWeight;

        // text appears within a hyperlink. change cursor to hand and make clicking open a new window
        if (currentHyperLink != null)
        {
          textblock.Cursor = Cursors.Hand;
          textblock.MouseLeftButtonDown += (s, e) => { HtmlPage.Window.Navigate(currentHyperLink.Uri, "_blank"); };
        }

        if (run.Blink)
        {
          //textblock.SetValue(Canvas.NameProperty, "TestTest");

          Storyboard sb = new Storyboard();

          sb.BeginTime = TimeSpan.FromSeconds(0);
          sb.RepeatBehavior = RepeatBehavior.Forever;

          DoubleAnimationUsingKeyFrames blinkAnimation = new DoubleAnimationUsingKeyFrames();
          blinkAnimation.Duration = TimeSpan.FromSeconds(3.5);
          blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(0)), Value = 1 });
          blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(2.5)), Value = 0 });
          blinkAnimation.KeyFrames.Add(new DiscreteDoubleKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(3.5)), Value = 1 });

          Storyboard.SetTarget(sb, textblock);
          blinkAnimation.SetValue(Storyboard.TargetPropertyProperty, "Opacity");

          sb.Children.Add(blinkAnimation);

          Resources.Add("BlinkStoryboard", sb);
          //sb.Begin();
        }

        AddLineContent(textblock);
      }
      else if (currentInline is TabControl)
      {
        double tabWidth = CalculateTabWidth();

        // add invisible spacer to simulate tab
        AddLineContent(CreateSpacer(tabWidth));
      }
      else if (currentInline is ImageInlineControl)
      {
        ImageInlineControl imageInlineControl = (ImageInlineControl)currentInline;

        Image image = new Image();
        image.Width = imageInlineControl.Width;
        image.Height = imageInlineControl.Height;
        imageInlineControl.SetImageSource(image);

        AddLineContent(image);
      }
      else if (currentInline is HyperLinkControl)
      {
        HyperLinkControl hyperLinkControl = (HyperLinkControl)currentInline;

        foreach (InlineControl inline in hyperLinkControl.Inlines)
        {
          AddInline(inline, hyperLinkControl);
        }
      }
    }

    private double CalculateTabWidth()
    {
      double tabWidth;
      TabStop currentTabStop = _tabStops.Where(t => t.Position > _currentLine.CustomDesiredSize.Width).FirstOrDefault();
      if (currentTabStop != null)
        tabWidth = currentTabStop.Position - _currentLine.CustomDesiredSize.Width;
      else
        tabWidth = _defaultTabStop - (_currentLine.CustomDesiredSize.Width % _defaultTabStop);

      return tabWidth;
    }

    private void MeasureCurrentLine()
    {
      _currentLine.CustomMeasure(new Size(_availableWidth, double.PositiveInfinity));
    }

    public void AddLineContent(UIElement visual)
    {
      if (_currentLine.Children.Count > 0 || _previousLine == null || _previousLine.WhiteSpaceEnd)
      {
        _currentLine.Children.Add(visual);
        //FrameworkElementBase.UpdateLayout(_currentLine);
      }
      else
      {
        IList<TextBlock> previousLineUnbrokenText = RemoveLineEndTextUntilSpace(_previousLine);

        _currentLine.Children.AddRange<UIElement>(previousLineUnbrokenText.Select(t => (UIElement)t));
        _currentLine.Children.Add(visual);
        //FrameworkElementBase.UpdateLayout(_currentLine);
      }

      MeasureCurrentLine();

      if (_currentLine.Lines > 1 || _currentLine.CustomDesiredSize.Width > _currentLine.MaxWidth)
      {
        if (visual is TextBlock)
        {
          _currentLine.Children.Remove(visual);

          TextBlock textBlock = (TextBlock)visual;
          TextBlock copyTextBlock = CloneTextBlock(textBlock);
          copyTextBlock.Text = string.Empty;
          _currentLine.Children.Add(copyTextBlock);

          IList<string> splitText = StringUtils.SplitOnWhiteSpace(textBlock.Text, false);

          string addedText = AddSplitText(copyTextBlock, splitText, true);

          if (_currentLine.Children.Where(e => e is TextBlock).Count() == 0 && addedText.Length == 0)
          {
            splitText = textBlock.Text.ToCharArray().Select(c => c.ToString()).ToList();

            addedText = AddSplitText(copyTextBlock, splitText, false);

            if (string.IsNullOrEmpty(addedText))
              addedText = textBlock.Text;
          }

          AddLine();

          string remainingText = textBlock.Text.Substring(addedText.Length);

          if (!string.IsNullOrEmpty(remainingText))
          {
            textBlock.Text = remainingText;
            AddLineContent(textBlock);
          }
        }
        //else if (visual is Image)
        //{
        //}
        else
        {
          if (_currentLine.Children.Count > 1)
          {
            _currentLine.Children.Remove(visual);

            AddLine();

            AddLineContent(visual);
          }
          else
          {
            AddLine();
          }
        }
      }
    }

    private TextBlock CloneTextBlock(TextBlock source)
    {
      return WpfUtils.Clone<TextBlock>(source,
        null,
        new List<PropertyInfo>()
        {
          typeof(TextBlock).GetProperty("LineHeight"),
          typeof(TextBlock).GetProperty("MaxHeight"),
          typeof(TextBlock).GetProperty("MinHeight"),
          typeof(TextBlock).GetProperty("MaxWidth"),
          typeof(TextBlock).GetProperty("MinWidth"),
          typeof(ResourceDictionary).GetProperty("Source")
        });
    }

    private IList<TextBlock> RemoveLineEndTextUntilSpace(ParagraphLine paragraphLine)
    {
      List<FrameworkElement> previousElements = new List<FrameworkElement>(paragraphLine.Children.Select(e => (FrameworkElement)e).Reverse());
      List<TextBlock> previousTextBlocks = new List<TextBlock>();

      foreach (FrameworkElement element in previousElements)
      {
        if (element is TextBlock)
        {
          TextBlock text = (TextBlock)element;
          IList<string> entries = StringUtils.SplitOnWhiteSpace(text.Text, false);

          if (entries.Count == 1)
          {
            paragraphLine.Children.Remove(text);
            previousTextBlocks.Add(text);
          }
          else
          {
            TextBlock newText = CloneTextBlock(text);
            newText.Text = entries.Last();
            text.Text = text.Text.Substring(0, text.Text.Length - newText.Text.Length);

            previousTextBlocks.Add(newText);
            break;
          }
        }
        else
        {
          break;
        }
      }

      return previousTextBlocks;
    }

    private string AddSplitText(TextBlock copyTextBlock, IList<string> splitText, bool addSpaces)
    {
      StringBuilder combinedText = new StringBuilder();

      bool space = false;

      for (int i = 0; i < splitText.Count; i++)
      {
        string text = splitText[i];

        if (i != 0 && addSpaces && !space)
          combinedText.Append(" ");

        space = string.IsNullOrEmpty(text);

        if (space)
          combinedText.Append(" ");
        else
          combinedText.Append(text);

        copyTextBlock.Text = combinedText.ToString();
        //_currentLine.IsDirty = true;
        //FrameworkElementBase.UpdateLayout(_currentLine);

        MeasureCurrentLine();

        if (_currentLine.Lines > 1 || _currentLine.CustomDesiredSize.Width > _currentLine.MaxWidth)
        {
          // return to state before line span
          combinedText.Remove(combinedText.Length - text.Length, text.Length);
          copyTextBlock.Text = combinedText.ToString().TrimEnd();
          //copyTextBlock.FontStretch = FontStretches.Condensed

          // no point leaving an empty textblock
          if (string.IsNullOrEmpty(copyTextBlock.Text))
            _currentLine.Children.Remove(copyTextBlock);
          else
            _currentLine.WhiteSpaceEnd = true;

          //_currentLine.IsDirty = true;
          //FrameworkElementBase.UpdateLayout(_currentLine);

          break;
        }
      }

      // return added text
      return combinedText.ToString();
    }

    //public Canvas Canvas
    //{
    //  get { return (Canvas)InnerElement; }
    //}

    public bool Split(Size availableSize, out FrameworkElement remainderElement)
    {
      //UpdateLayout();

      int lastIndex;
      double totalHeight = 0;
      for (lastIndex = 0; lastIndex < LineContainer.Children.Count; lastIndex++)
      {
        UserControlExtended element = LineContainer.Children[lastIndex] as UserControlExtended;
        //Point p = LineContainer.TransformToVisual(element).Transform(LayoutUtils.Zero);
        //Rect elementBounds = LayoutUtils.CreateRect(p, LineContainer.CustomDesiredSize);
        totalHeight += element.CustomDesiredSize.Height;

        if (totalHeight > availableSize.Height)
          break;
      }

      if (lastIndex > 0 && lastIndex < LineContainer.Children.Count)
      {
        _hasSplit = true;
        ParagraphControl remainderParagraph = (ParagraphControl)Clone();
        remainderParagraph.HasSplit = true;
        remainderParagraph.LineContainer.Children.Clear();

        int childrenCount = LineContainer.Children.Count;
        for (int i = lastIndex; i < childrenCount; i++)
        {
          FrameworkElement currentElement = (FrameworkElement)LineContainer.Children[lastIndex];
          LineContainer.Children.RemoveAt(lastIndex);
          //LineContainer.UpdateLayout();

          remainderParagraph.LineContainer.Children.Add(currentElement);
          //remainderParagraph.IsDirty = true;
        }

        remainderElement = remainderParagraph;
        return true;
      }

      remainderElement = null;
      return false;
    }

    public override string ToString()
    {
      StringBuilder sb = new StringBuilder();

      foreach (ParagraphLine line in LineContainer.Children)
      {
        sb.AppendLine(line.ToString());
      }

      return sb.ToString();
    }

    public object Clone()
    {
      Cloner cloner = new Cloner();
      cloner.IgnoredProperties.Add(typeof(ParagraphControl).GetProperty("Inlines"));
      //cloner.IgnoredProperties.Add(typeof(ParagraphControl).GetProperty("Children"));
      cloner.IgnoredProperties.Add(typeof(System.Windows.Controls.Control).GetProperty("Template"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("Resources"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("MaxWidth"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("MinWidth"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("MaxHeight"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("MinHeight"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("Style"));
      cloner.IgnoredProperties.Add(typeof(FrameworkElement).GetProperty("Language"));
      cloner.IgnoredProperties.Add(typeof(UIElement).GetProperty("RenderSize"));
      cloner.IgnoredProperties.Add(typeof(Matrix).GetProperty("Identity"));
      cloner.TypeInstantiationHandlers[typeof(Border)] = o => new Border(/*((Border)o).Control*/);
      cloner.TypeInstantiationHandlers[typeof(ParagraphControl)] = o => { ParagraphControl p = new ParagraphControl(); /*p.BeginUpdate();*/ return p; };
      cloner.TypeInstantiationHandlers[typeof(FontFamily)] = o => (FontFamily)o;
      cloner.TypeInstantiationHandlers[typeof(XmlLanguage)] = o => (XmlLanguage)o;

      ParagraphControl clone = (ParagraphControl)cloner.Clone(this);
      //clone.EndUpdate();
      return clone;
    }

    bool cloner_HandleError(System.Reflection.PropertyInfo property, Exception error)
    {
      if (error is InvalidOperationException || error is ArgumentException)
      {
        if (property.DeclaringType == typeof(Size) || property.DeclaringType == typeof(Rect))
          return true;
      }

      return false;
    }
  }
}